07 變數存在的泡泡空間──作用域

2022-09-22

介紹完變數後,來看看變數所處的環境。

用最簡單的方式來說,所謂的作用域可以理解為「變數能夠存活的區塊泡泡」。每個變數只屬於一個作用域,一個作用域內則可以存在多個變數。

執行程式時,通常會有複數個作用域同時存在。每個作用域內的程式碼,分別依循該作用域的查找規則存取變數。

程式只能對合法、有開放的作用域進行變數的取值和賦值,如果在當前有效的作用域範圍中無法找到對應變數,程式就會回報 ReferenceError,表示作用域解析失敗。


作用域類型

作用域主要分成以下兩種:

  • 詞法作用域(Lexical Scope),又稱靜態作用域(Static Scope)
  • 動態作用域(Dynamic Scope)

以下分別詳述:

詞法作用域 /語彙範疇(Lexical Scope)

  • 也稱作「靜態作用域/靜態範疇(Static Scope)」
  • 內部定義的變數叫做「詞法變數(Lexical Variables)」
  • 作用域在「編寫」時被定義
  • 取決於如何宣告,以及宣告的位置
  • 查找時以「程式碼的巢狀結構」為基礎

動態作用域/動態範疇(Dynamic Scope)

  • 內部定義的變數叫做「動態變數(Dynamic Variables)」
  • 作用域在「執行/呼叫/調用」時被動態決定
  • 取決於函式如何被調用,與宣告時所處的位置無關
  • 查找時以「呼叫堆疊(call stack)」為基礎

JavaScript 中的 witheval() 能夠修改詞法作用域,達成動態作用域的效果,但由於修改詞法作用域會導致編譯時的最佳化,如靜態分析等失效,造成效能降低問題。因此 with 目前已被廢除,而 eval() 在 MDN 文件中被強烈建議不要使用。

JS 的作用域類型

JavaScript 所採用的是詞法作用域,因此之後的討論都屬於詞法作用域的範疇。

JavaScript 「沒有」動態作用域,但 this 的作用域取決於函式的調用方式,其查找規則擁有與動態作用域相似的機制。


作用域規則

  • 通過識別字(identifier)查詢變數儲存的資料內容
  • 每個作用域內的變數名稱必須是唯一的
  • 相同的變數名稱可以出現在不同作用域中
  • 每個函式都擁有自己的作用域
  • 每個區塊({}所包覆的區域範圍)可以擁有自己的作用域(ES6+)
  • 每個作用域都是獨立的,沒有交集的部分
  • 只有作用域內部的程式碼才能取得該作用域內的變數
  • 一個作用域可以嵌套在另一個作用域中,這種多層結構稱為巢狀作用域(Nested Scope),其規則見下方

巢狀作用域(Nested Scope)

  • 作用域有時會將另一個作用域包含在內,形成巢狀作用域。
  • 在直接作用域(當前的作用域)中找不到某個變數時,引擎會諮詢下一個外層作用域,直到找到該變數,或者抵達最外層(全域作用域)為止。
  • 宣告變量後,它在這個作用域「以內」任何地方皆可使用,但在上層/外部作用域無法存取。也就是說,被嵌套在內部的作用域,其代碼可以取得外部作用域的變量,反之則無法。
  • 在任意作用域內替未宣告的變數賦值,則此變數會以全域變數被創建。

作用域種類

  • 全域作用域 Global Scope
    • 程式最外層的作用域,有時簡稱為 global
  • 函式作用域 Function Scope
    • 隨著函式一起產生的作用域
  • 區塊作用域 Block Scope (ES6+)
    • {} 以內的區塊,如 ifwhilefor
    • var 沒有區塊作用域,因此定義變數時需要使用 letconst

有部分觀點認為,除了通用的常數與少數例外,由於使用困難、不易維護等原因,Global Scope 內不應該存在任何自訂變數。

由於 var 沒有區塊作用域,有時會意外導致變數洩露到外層作用域,因此也有人提倡全面廢止 var,僅使用 letconst


所以作用域到底是?

用簡單的概念來理解,作用域就是「能夠存取變數的範圍」,不過實際執行時要更複雜一些,用更加嚴謹的方式來說:

「作用域是指一組變數的集合,以及這些變數該如何被查找的規則。」

這些規則與判斷讓程式知道該如何存取變數,從而建構出我們所理解的「作用域」。

而與 JS 作用域相關的規則,則會在後續的文章中一一解析。


參考資料

關於我GithubCopyright © Emi 2022